﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;

namespace LeRenard.Core.Ui.ConsoleUtils
{
    // TODO:
    // - Add wrap option
    //   http://stackoverflow.com/questions/4398270/how-to-split-string-preserving-whole-words

    /// <summary>
    /// Holds settings & methods for the current console session.
    /// </summary>
    public static class ConsoleSession
    {
        #region Construction
        /// <summary>
        /// Default settings.
        /// </summary>
        static ConsoleSession()
        {
            _indentationLevel = 0;
            _indentationSize = 2;
            _indentationStringCache = new Dictionary<string, string>();

            Font.DefaultColor = Console.ForegroundColor;
            Font.ErrorColor = ConsoleColor.Red;

            Background.DefaultColor = Console.BackgroundColor;
        }
        #endregion

        #region General - Indentation
        /// <summary>
        /// Gets/sets the current indentation level.
        /// </summary>
        public static int IndentationLevel { get { return _indentationLevel; } }
        private static int _indentationLevel;

        /// <summary>
        /// Gets/sets the indentation (character) size.
        /// </summary>
        private static int IndentationSize
        {
            get {
                return _indentationSize;
            }
            set {
                _indentationSize = value;
                _indentationStringCache.Clear();
            }
        }
        private static int _indentationSize;

        /// <summary>
        /// Holds the indentation string cache.
        /// </summary>
        private static IDictionary<string, string> _indentationStringCache;

        /// <summary>
        /// Calculates the current indentation key, used by the caching algorithm.
        /// </summary>        
        private static string CalculateIndentationKey(char @char)
        {
            return CalculateIndentationKey(@char.ToString());
        }

        /// <summary>
        /// Calculates the current indentation key, used by the caching algorithm.
        /// </summary>        
        private static string CalculateIndentationKey(string @string)
        {
            return CalculateIndentationKey(_indentationLevel, @string);
        }

        /// <summary>
        /// Calculates the current indentation key, used by the caching algorithm.
        /// </summary>        
        private static string CalculateIndentationKey(int indentationLevel, string @string)
        {
            var indentationKey = "{0}.{1}"
                .FormatWith(
                    indentationLevel,
                    @string
                );

            return indentationKey;
        }

        /// <summary>
        /// Indents by one.
        /// </summary>
        public static void Indent()
        {
            _indentationLevel++;            
        }

        /// <summary>
        /// Unindents by one.
        /// </summary>
        public static void UnIndent()
        {
            if (_indentationLevel > 0) {
                _indentationLevel--;                
            }
        }

        /// <summary>
        /// Gets the indentation string by a given character.
        /// </summary>
        /// <param name="char">The character to use when creating the indentation string.</param>        
        public static string GetIndentationString(char @char)
        {
            return GetIndentationString(@char.ToString());
        }

        /// <summary>
        /// Gets the indentation string by a given character.
        /// </summary>
        /// <param name="string">The character to use when creating the indentation string.</param>
        /// <returns></returns>
        public static string GetIndentationString(string @string)
        {
            var indentationKey = CalculateIndentationKey(@string);

            if (!_indentationStringCache.Keys.Contains(indentationKey)) {
                var indentationString = new StringBuilder();

                for (int i = 0; i < _indentationLevel; i++) {
                    for (int s = 0; s < _indentationSize; s++) {
                        indentationString.Append(@string);
                    }
                }

                _indentationStringCache[indentationKey] = indentationString.ToString();
            }

            return _indentationStringCache[indentationKey];
        }
        #endregion

        #region General - Write-methods
        /// <summary>
        /// Writes out the given exception.
        /// </summary>
        /// <param name="exception">The exception to write.</param>        
        /// <param name="includeInnerException">Determines whether to write out inner exceptions.</param>        
        public static void WriteException(Exception exception, bool includeInnerException = true)
        {
            Font.ChangeColor(Font.ErrorColor);
            Console.WriteLine(
                "{0}[EXC:{1}] {2}",
                GetIndentationString(' '),      
                exception.GetType().ToFriendlyName(),
                exception.Message
            );            

            Font.ChangeColor(ConsoleColor.DarkRed);
            if (!String.IsNullOrEmpty(exception.StackTrace)) {
                Console.WriteLine(
                    "{0}{1}",
                    GetIndentationString(' '),
                    exception.StackTrace
                );
            }

            Font.ChangeColor();

            if (includeInnerException && exception.InnerException != null) {
                Indent();
                WriteException(exception.InnerException, includeInnerException);
                UnIndent();
            }
        }       

        /// <summary>
        /// Writes out a blank line for x-<paramref="amount"/> times.
        /// </summary>
        /// <param name="amount">Amount of times to write out a blank line.</param>
        public static void WriteLine(int amount)
        {
            for (int i = 0; i < amount; i++) {
                Console.WriteLine();
            }
        }
        #endregion

        #region General - Colors
        /// <summary>
        /// Returns the closest <see cref="ConsoleColor"/> for a <see cref="Color"/>.
        /// </summary>
        public static ConsoleColor ClosestConsoleColor(Color color)
        {
            ConsoleColor ret = 0;
            double r = color.R;
            double g = color.G;
            double b = color.B;
            double delta = double.MaxValue;

            foreach (ConsoleColor cc in Enum.GetValues(typeof(ConsoleColor))) {
                var n = Enum.GetName(typeof(ConsoleColor), cc);
                var c = Color.FromName(n == "DarkYellow" ? "Orange" : n); // bug fix
                var t = Math.Pow(c.R - r, 2.0) + Math.Pow(c.G - g, 2.0) + Math.Pow(c.B - b, 2.0);
                if (t == 0.0)
                    return cc;
                if (t < delta) {
                    delta = t;
                    ret = cc;
                }
            }
            return ret;
        }
        #endregion

        #region Font
        /// <summary>
        /// Holds font settings for the current console session.
        /// </summary>
        public static class Font
        {
            /// <summary>
            /// Gets/sets the default color.
            /// </summary>
            public static ConsoleColor DefaultColor { get; set; }

            /// <summary>
            /// Gets/sets the error color.
            /// </summary>
            public static ConsoleColor ErrorColor { get; set; }

            /// <summary>
            /// Changes the currently used color.
            /// </summary>
            /// <param name="newColor">The new color, if null it will use the <see cref="DefaultColor"/> property.</param>
            public static void ChangeColor(ConsoleColor? newColor = null)
            {
                Console.ForegroundColor = newColor ?? DefaultColor;
            }
        }        
        #endregion

        #region Background
        public static class Background
        {
            /// <summary>
            /// Gets/sets the default color.
            /// </summary>
            public static ConsoleColor DefaultColor { get; set; }

            /// <summary>
            /// Changes the currently used color.
            /// </summary>
            /// <param name="newColor">The new color, if null it will use the <see cref="DefaultColor"/> property.</param>
            public static void ChangeColor(ConsoleColor? newColor = null)
            {
                Console.BackgroundColor = newColor ?? DefaultColor;
            }
        }
        #endregion
    }
}
